﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using Jayrock.Json;
using System.IO;
using System.Globalization;
using System.Xml;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;

namespace StackExchangeApi
{
    /// <summary>
    /// Represents a user on one of the "trilogy" sites
    /// </summary>
    public sealed class User : ICached
    {
        private Object m_values_sync = new Object();
        private Object m_favs_sync = new Object();
        private Object m_answers_sync = new Object();
        private Object m_questions_sync = new Object();

        [BacksCache]
        private volatile ReadOnlyCollection<Question> m_favorite;

        [BacksCache]
        private volatile bool m_reputation_set = false;
        private long m_reputation;

        [BacksCache]
        private volatile String m_name;

        [BacksCache]
        private volatile ReadOnlyCollection<Answer> m_answers;

        [BacksCache]
        private volatile ReadOnlyCollection<Question> m_questions;

        [BacksCache]
        private volatile Uri m_gravatar;

        [BacksCache]
        private volatile bool m_gold_badge_count_set;
        private long m_gold_badge_count;

        [BacksCache]
        private volatile bool m_silver_badge_count_set;
        private long m_silver_badge_count;

        [BacksCache]
        private volatile bool m_bronze_badge_count_set;
        private long m_bronze_badge_count;

        private Object m_recent_sync = new Object();
        [BacksCache]
        private volatile ReadOnlyCollection<Post> m_recent_activity;

        private Object m_badges_sync = new Object();
        [BacksCache]
        private volatile ReadOnlyCollection<Badge> m_badges;

        /// <summary>
        /// Converts an EagerUser into a User.
        /// 
        /// Although the User would normally be lazily-loaded,
        /// since it is initialized with a fully populated EagerUser
        /// no loading at all will occur when its properties are
        /// accessed.
        /// </summary>
        /// <param name="user">EagerUser to cast</param>
        /// <returns>A User as above.</returns>
        public static implicit operator User(EagerUser user)
        {
            return UserCache.GetOrAllocate(user.Id, user.Site);
        }

        /// <summary>
        /// Site this user is a member of
        /// </summary>
        public StackExchangeSite Site { get; internal set; }

        /// <summary>
        /// Id of this User
        /// </summary>
        public long Id
        {
            get;
            internal set;
        }

        /// <summary>
        /// The Badges earned by this User.
        /// 
        /// If the User has earned a particular Badge
        /// more than once, it will be including multiple times
        /// accordingly.
        /// </summary>
        public ReadOnlyCollection<Badge> Badges
        {
            get
            {
                if (m_badges == null)
                {
                    lock (m_badges_sync)
                    {
                        if (m_badges == null)
                            Badge.SetBadgesForUser(this);
                    }
                }

                return m_badges;
            }

            internal set
            {
                m_badges = value;
            }
        }

        /// <summary>
        /// The recent activity of this User.
        /// 
        /// Contains a list of Answers, Questions, and Comments.
        /// 
        /// Size is determined by the "Trilogy Site".
        /// </summary>
        public ReadOnlyCollection<Post> RecentActivity
        {
            get
            {
                if (m_recent_activity == null)
                {
                    lock (m_recent_sync)
                    {
                        if(m_recent_activity == null)
                            SetUserRecent(this);
                    }
                }

                return m_recent_activity;
            }

            internal set
            {
                m_recent_activity = value;
            }
        }

        /// <summary>
        /// ReputationGraph for this User.
        /// Allows access to all reputation modifying
        /// events in this User's history based on the time
        /// they occured.
        /// </summary>
        public ReputationGraph ReputationGraph
        {
            get;
            internal set;
        }

        /// <summary>
        /// Name of this user
        /// </summary>
        public String Name
        {
            get
            {
                if (m_name == null)
                {
                    lock (m_values_sync)
                    {
                        if (m_name == null)
                        {
                            SetUserValues(this);
                        }
                    }
                }

                return m_name;
            }

            internal set { m_name = value; }
        }

        /// <summary>
        /// List of answers this User has posted
        /// </summary>
        public ReadOnlyCollection<Answer> Answers
        {
            get
            {
                if (m_answers == null)
                {
                    lock (m_answers_sync)
                    {
                        if (m_answers == null)
                            Answer.SetAnswersFor(this);
                    }
                }

                return m_answers;
            }

            internal set
            {
                m_answers = value;
            }
        }

        /// <summary>
        /// List of questions this User has asked
        /// </summary>
        public ReadOnlyCollection<Question> Questions
        {
            get {
                if (m_questions == null)
                {
                    lock (m_questions_sync)
                    {
                        if (m_questions == null)
                            Question.SetQuestionsForUser(this);
                    }
                }

                return m_questions;
            }
            
            internal set { m_questions = value; }
        }

        /// <summary>
        /// List of questions this User has Favorited
        /// </summary>
        public ReadOnlyCollection<Question> FavoriteQuestions
        {
            get
            {
                if (m_favorite == null)
                {
                    lock (m_favs_sync)
                    {
                        if (m_favorite == null)
                            SetUserFavorites(this);
                    }
                }

                return m_favorite;
            }

            internal set
            {
                m_favorite = value;
            }
        }

        /// <summary>
        /// The Uri to pull a User's gravatar from.
        /// </summary>
        public Uri Gravatar
        {
            get
            {
                if (m_gravatar == null)
                {
                    lock (m_values_sync)
                    {
                        if (m_gravatar == null)
                            SetUserValues(this);
                    }
                }

                return m_gravatar;
            }

            set
            {
                m_gravatar = value;
            }
        }

        /// <summary>
        /// Reputation this user has
        /// </summary>
        public long Reputation
        {
            get
            {
                if (!m_reputation_set)
                {
                    lock (m_values_sync)
                    {
                        if (!m_reputation_set)
                        {
                            SetUserValues(this);
                        }
                    }
                }

                return m_reputation;
            }

            internal set
            {
                m_reputation = value;
                m_reputation_set = true;
            }
        }

        /// <summary>
        /// The number of gold badges this User has
        /// </summary>
        public long GoldBadgeCount
        {
            get
            {
                if (!m_gold_badge_count_set)
                {
                    lock (m_values_sync)
                        if (!m_gold_badge_count_set)
                            SetUserValues(this);
                }

                return m_gold_badge_count;
            }

            internal set
            {
                m_gold_badge_count = value;
                m_gold_badge_count_set = true;
            }
        }

        /// <summary>
        /// The number of silver badges this User has
        /// </summary>
        public long SilverBadgeCount
        {
            get
            {
                if (!m_silver_badge_count_set)
                    lock (m_values_sync)
                        if (!m_silver_badge_count_set)
                            SetUserValues(this);

                return m_silver_badge_count;
            }

            internal set
            {
                m_silver_badge_count = value;
                m_silver_badge_count_set = true;
            }
        }

        /// <summary>
        /// The number of bronze badges this User has.
        /// </summary>
        public long BronzeBadgeCount
        {
            get
            {
                if (!m_bronze_badge_count_set)
                    lock (m_values_sync)
                        if (!m_bronze_badge_count_set)
                            SetUserValues(this);

                return m_bronze_badge_count;
            }

            internal set
            {
                m_bronze_badge_count = value;
                m_bronze_badge_count_set = true;
            }
        }

        /// <summary>
        /// Construct a user with the given UID
        /// </summary>
        /// <param name="id">unique identifier for the user</param>
        /// <param name="site">site this user belongs to</param>
        internal User(long id, StackExchangeSite site)
        {
            Id = id;
            Site = site;

            ReputationGraph = new ReputationGraph(this);
        }

        /// <summary>
        /// Build a user with the given UID.
        /// 
        /// If the uid is invalid, calls ON the User object will fail but construction
        /// will succeed.
        /// </summary>
        /// <param name="id">unique identifier for the user</param>
        /// <param name="site">site the user belongs to</param>
        /// <returns>A new user object, irregardless of existance of the user</returns>
        public static User GetUserWithId(long id, StackExchangeSite site)
        {
            return UserCache.GetOrAllocate(id, site);
        }

        /// <summary>
        /// Searches for the given username, then returns a User for
        /// each found user.
        /// 
        /// Note that searching is case insensitive, and there is a cap on the returned
        /// number of users.
        /// 
        /// Don't search for "K" and expect all users with "K" in there name, basically.
        /// </summary>
        /// <param name="name">Name to search for</param>
        /// <param name="site">Site to search</param>
        /// <returns>A list of users found</returns>
        public static ICollection<User> GetUsersForName(String name, StackExchangeSite site)
        {
            List<User> ret = new List<User>();
            String text = ConnectionMediator.MakeRequest(GetUserSearchUrl(name, site));

            String[] entries = Regex.Split(text, "<div class=\"user-details\">");

            for (int i = 1; i < entries.Length; i++)
            {
                String entry = entries[i];

                int p = entry.IndexOf("href=\"", StringComparison.Ordinal);

                p += 6;

                int q = entry.IndexOf("\"", p, StringComparison.Ordinal);

                String uri = entry.Substring(p, q - p);
                String[] parts = uri.Split(new char[] { '/' });

                User user = User.GetUserWithId(Int64.Parse(parts[2], CultureInfo.InvariantCulture), site);

                ret.Add(user);
            }

            return ret;
        }

        /// <summary>
        /// Builds a Uri for searching for users on one of the
        /// StackExchange sites
        /// </summary>
        /// <param name="username">Username to search for</param>
        /// <param name="site">Site to search</param>
        /// <returns>A Uri constructed that will return a search result, as an HTML page</returns>
        private static Uri GetUserSearchUrl(String username, StackExchangeSite site)
        {
            String s = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/users/filter/{1}", 
                Utils.GetAddressForSite(site), 
                username.ToLowerInvariant());

            return new Uri(s);
        }

        /// <summary>
        /// Constructs a uri that gets the JSON version of a User's flair.
        /// </summary>
        /// <param name="uid">id of the User in question</param>
        /// <param name="site">Site the User is a member of</param>
        /// <returns>As above</returns>
        private static Uri GetUserFlairUrl(long uid, StackExchangeSite site)
        {
            String s = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/users/flair/{1}.json", 
                Utils.GetAddressForSite(site), 
                uid);

            return new Uri(s);
        }

        /// <summary>
        /// Populate the name and reputation values for the given user
        /// 
        /// Uses the flair json call.
        /// </summary>
        /// <param name="user">User to populate.</param>
        private static void SetUserValues(User user)
        {
            Uri req = GetUserFlairUrl(user.Id, user.Site);

            String json = ConnectionMediator.MakeRequest(req);
            JsonReader reader = new JsonTextReader(new StringReader(json));

            JsonObject obj = new JsonObject();

            try
            {
                obj.Import(reader);
            }
            catch(Exception e)
            {
                //Explicitly check this operation, as the Trilogy sites will redirect malformed
                //  flair requests to HTML pages.  This is kind of goofy, in that we won't
                //  realize a failure has occured until we try and parse it.
                throw new TrilogySiteDownloadException(req, e);
            }

            user.Name = (String)obj["displayName"];

            long rep;

            String s = (String)obj["reputation"];
            String ss = "";
            for (int i = 0; i < s.Length; i++)
                if (Char.IsDigit(s[i])) ss += s[i];

            rep = Int64.Parse(ss, CultureInfo.InvariantCulture);

            user.Reputation = rep;

            String grav = (String)obj["gravatarHtml"];
            int a = grav.IndexOf("src=\"", StringComparison.Ordinal);
            a += 5;
            int b = grav.IndexOf("\"", a, StringComparison.Ordinal);

            String gravUri = grav.Substring(a, b - a);

            user.Gravatar = new Uri(gravUri);

            String badgesTxt = (String)obj["badgeHtml"];

            Dictionary<String, long> badges = new Dictionary<String, long>();

            String[] titles = Regex.Split(badgesTxt, "title=\"");

            for (int j = 1; j < titles.Length; j++ )
            {
                String title = titles[j];

                int p = title.IndexOf("\"", StringComparison.Ordinal);
                String text = title.Substring(0, p);

                int i = text.IndexOf(" ", StringComparison.Ordinal);
                long count = long.Parse(text.Substring(0, i), CultureInfo.InvariantCulture);

                if (text.IndexOf("gold", StringComparison.Ordinal) != -1)
                    badges["gold"] = count;

                if (text.IndexOf("silver", StringComparison.Ordinal) != -1)
                    badges["silver"] = count;

                if (text.IndexOf("bronze", StringComparison.Ordinal) != -1)
                    badges["bronze"] = count;
            }

            if (badges.ContainsKey("gold"))
                user.GoldBadgeCount = badges["gold"];
            else
                user.GoldBadgeCount = 0;

            if (badges.ContainsKey("silver"))
                user.SilverBadgeCount = badges["silver"];
            else
                user.SilverBadgeCount = 0;

            if (badges.ContainsKey("bronze"))
                user.BronzeBadgeCount = badges["bronze"];
            else
                user.BronzeBadgeCount = 0;
        }

        /// <summary>
        /// Loads the User's favorite questions, and sets the FavoriteQuestions fields.
        /// </summary>
        /// <param name="user">User in question</param>
        private static void SetUserFavorites(User user)
        {
            int pageNum = 1;

            List<Question> questions = new List<Question>();

            bool done = false;

            while (!done)
            {
                String json = ConnectionMediator.MakeRequest(GetFavoritesUrl(user, pageNum));

                JsonArray posts = new JsonArray();
                posts.Import(new JsonTextReader(new StringReader(json)));

                if (posts.Length == 0) break;

                foreach (JsonObject post in posts)
                {
                    long id = ((JsonNumber)post["Id"]).ToInt64();
                    long favCount = ((JsonNumber)post["FavCount"]).ToInt64();
                    long votes = ((JsonNumber)post["VoteCount"]).ToInt64();
                    long answerCount = ((JsonNumber)post["AnswerCount"]).ToInt64();
                    long viewCount = ((JsonNumber)post["ViewCount"]).ToInt64();

                    String title = (String)post["Title"];
                    String tagStr = (String)post["Tags"];
                    tagStr = tagStr.Replace("\u003c", "").Replace("\u003e", " ");
                    List<String> tags = new List<string>(tagStr.Split(' '));

                    long createdTicks = 0;
                    if (!(post["CreatedDate"] is JsonNull) && post["CreatedDate"] != null)
                    {
                        String create = (String)post["CreatedDate"];
                        int a = create.IndexOf('(') + 1;
                        int b = create.IndexOf(')');
                        String cStr = create.Substring(a, b - a);

                        createdTicks = Int64.Parse(cStr, CultureInfo.InvariantCulture);
                    }

                    long lastEditTicks = 0;
                    if (!(post["LastEditDate"] is JsonNull) && post["LastEditDate"] != null)
                    {
                        String edit = (String)post["LastEditDate"];
                        int a = edit.IndexOf('(') + 1;
                        int b = edit.IndexOf(')');
                        String eStr = edit.Substring(a, b - a);

                        lastEditTicks = Int64.Parse(eStr, CultureInfo.InvariantCulture);
                    }

                    DateTime created;
                    if (createdTicks != 0)
                        created = new DateTime(createdTicks);
                    else
                        created = DateTime.MinValue;

                    DateTime lastEdit;
                    if (lastEditTicks != 0)
                        lastEdit = new DateTime(lastEditTicks);
                    else
                        lastEdit = DateTime.MinValue;

                    //If we've seen this question before
                    //  TODO:  This is less than elegant, consider a different approach
                    bool externBreak = false;
                    foreach (Question question in questions)
                        if (question.Id == id)
                        {
                            externBreak = true;
                            break;
                        }

                    if (externBreak)
                    {
                        done = true;
                        break;
                    }

                    Question questionObj = Question.GetQuestionWithId(id, user.Site);
                    questionObj.ViewCount = viewCount;
                    questionObj.Created = created;
                    questionObj.LastEdit = lastEdit;
                    questionObj.Tags = tags.AsReadOnly();
                    questionObj.AnswerCount = answerCount;
                    questionObj.FavoriteCount = favCount;
                    questionObj.Title = title;
                    questionObj.Votes = votes;

                    questions.Add(questionObj);

                    pageNum++;
                }
            }

            user.FavoriteQuestions = questions.AsReadOnly();
        }

        /// <summary>
        /// Constructs a uri that, when fetched, gets a JSON encoding
        /// of a given page of the user's favorite questions.
        /// </summary>
        /// <param name="user">User in question</param>
        /// <param name="pageNum">Page to fetch</param>
        /// <returns>As above</returns>
        private static Uri GetFavoritesUrl(User user, int pageNum)
        {
            String uri = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/api/userfavorites.json?userid={1}&page={2}&pagesize={3}&sort={4}",
                Utils.GetAddressForSite(user.Site),
                user.Id,
                pageNum,
                50,
                "recent");

            return new Uri(uri);
        }

        /// <summary>
        /// Parses the given Xml entry node, expecting one from a user's recent activity field.
        /// 
        /// Returns a SXAPI object that represents the entry.
        /// Either an Answer, a Question, or a Comment.
        /// 
        /// Delegates most of the work to Parse[Question/Answer/Comment]().
        /// </summary>
        /// <param name="entry">Xml "entry" node</param>
        /// <param name="site">Site entry originated from</param>
        /// <returns>Either an Answer, a Question, or a Comment as dictated by entry</returns>
        private static Post ParsePostEntry(XmlNode entry, StackExchangeSite site)
        {
            Dictionary<String, String> vals = new Dictionary<string, string>();

            Answer.ParseValues(entry, vals);

            if (vals["TYPE"] == "question")
            {
                List<String> tags = new List<String>();
                foreach (XmlNode child in entry.ChildNodes)
                    if (child.Name == "category")
                        tags.Add(child.Attributes["term"].InnerText);

                return ParseQuestion(vals, tags, site);
            }

            if (vals["TYPE"] == "answer")
            {
                return ParseAnswer(vals, site);
            }

            if (vals["TYPE"] == "comment")
            {
                return ParseComment(vals, site);
            }

            throw new ArgumentException("Entry is of an unrecognized type");
        }

        /// <summary>
        /// Construct a Question using key->value pairs, a list of tags, and a site.
        /// </summary>
        /// <param name="vals">key->value pairs as extracted by Answer.ParseValues</param>
        /// <param name="tags">list of tags the Question has</param>
        /// <param name="site">site the Question is on</param>
        /// <returns>A corresponding Question</returns>
        private static Question ParseQuestion(Dictionary<String, String> vals, List<String> tags, StackExchangeSite site)
        {
            String qStr = "/questions/";
            int p = vals["id"].IndexOf(qStr, StringComparison.Ordinal);
            p += qStr.Length;
            int q = vals["id"].IndexOf("/", p, StringComparison.Ordinal);

            long id = Int64.Parse(vals["id"].Substring(p, q - p), CultureInfo.InvariantCulture);

            Question question = Question.GetQuestionWithId(id, site);
            question.Title = vals["title"];
            question.Votes = Int64.Parse(vals["re:rank"], CultureInfo.InvariantCulture);
            question.Author = vals["author"];
            question.Created = DateTime.ParseExact(vals["published"], "u", DateTimeFormatInfo.InvariantInfo);
            question.LastEdit = DateTime.ParseExact(vals["updated"], "u", DateTimeFormatInfo.InvariantInfo);

            question.Summary = vals["summary"];

            question.Tags = tags.AsReadOnly();

            return question;
        }

        /// <summary>
        /// Construct an Answer given a set of key->value pairs and a site.
        /// </summary>
        /// <param name="vals">key->value pairs as extract by Answer.ParseValues</param>
        /// <param name="site">Site the Answer is on</param>
        /// <returns>A corresponding Answer</returns>
        private static Answer ParseAnswer(Dictionary<String, String> vals, StackExchangeSite site)
        {
            int p = vals["id"].LastIndexOf("#", StringComparison.Ordinal);
            p += 1;

            long id = Int64.Parse(vals["id"].Substring(p), CultureInfo.InvariantCulture);

            Answer answer = Answer.GetAnswerWithId(id, site);
            answer.Title = vals["title"];
            answer.Votes = Int64.Parse(vals["re:rank"], CultureInfo.InvariantCulture);
            answer.Author = vals["author"];
            answer.Published = DateTime.ParseExact(vals["published"], "u", DateTimeFormatInfo.InvariantInfo);
            answer.Updated = DateTime.ParseExact(vals["updated"], "u", DateTimeFormatInfo.InvariantInfo);

            answer.Summary = vals["summary"];

            return answer;
        }

        /// <summary>
        /// Construct a Comment out of the given key->value pairs.
        /// </summary>
        /// <param name="vals">pairs as extracted by Answer.ParseValues</param>
        /// <param name="site">Site the comment is on</param>
        /// <returns>A corresponding Comment</returns>
        private static Comment ParseComment(Dictionary<String, String> vals, StackExchangeSite site)
        {
            bool isAnswer = vals["id"].IndexOf("#", StringComparison.Ordinal) != -1;

            Post on;

            if (isAnswer)
            {
                int p = vals["id"].LastIndexOf("#", StringComparison.Ordinal);
                p += 1;
                on = new Answer(
                    Int64.Parse(vals["id"].Substring(p), CultureInfo.InvariantCulture),
                    site);
            }
            else
            {
                String qStr = "/questions/";
                int p = vals["id"].IndexOf(qStr, StringComparison.Ordinal);
                p += qStr.Length;
                int q = vals["id"].IndexOf("/", p, StringComparison.Ordinal);

                on = new Question(
                    Int64.Parse(vals["id"].Substring(p, q - p), CultureInfo.InvariantCulture),
                    site);
            }

            return new Comment(
                vals["title"],
                vals["author"],
                on,
                DateTime.ParseExact(vals["published"], "u", DateTimeFormatInfo.InvariantInfo),
                DateTime.ParseExact(vals["updated"], "u", DateTimeFormatInfo.InvariantInfo),
                vals["summary"],
                site);
        }

        /// <summary>
        /// Sets the RecentActivity property on the given User.
        /// </summary>
        /// <param name="user">User in question</param>
        internal static void SetUserRecent(User user)
        {
            String feedStr = ConnectionMediator.MakeRequest(GetUserFeedUrl(user));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(feedStr);

            XmlNode feed = doc.FirstChild.NextSibling;

            List<Post> posts = new List<Post>();

            foreach (XmlNode node in feed.ChildNodes)
            {
                if (node.Name == "entry")
                {
                    posts.Add(ParsePostEntry(node, user.Site));
                }
            }

            user.RecentActivity = posts.AsReadOnly();
        }

        /// <summary>
        /// Returns a URI that when fetched gives the RSS feed
        /// of the provided user's recent activity.
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        private static Uri GetUserFeedUrl(User user)
        {
            String uri = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/feeds/user/{1}",
                Utils.GetAddressForSite(user.Site),
                user.Id
                );

            return new Uri(uri);
        }

        /// <summary>
        /// Custom Equality operation.
        /// 
        /// Note that it IS possible for an EagerUser to be
        /// equivalent to a User.
        /// </summary>
        /// <param name="obj">To test for equality</param>
        /// <returns>true if equal, false otherwise</returns>
        public override bool Equals(object obj)
        {
            User otherU = obj as User;
            EagerUser otherEU = obj as EagerUser;

            if (otherU != null)
            {
                return otherU.Site.Equals(Site) &&
                       otherU.Id.Equals(Id);
            }

            if (otherEU != null)
            {
                return otherEU.Site.Equals(Site) &&
                       otherEU.Id.Equals(Id);
            }

            return false;
        }

        /// <summary>
        /// Custom hash-code implementation
        /// </summary>
        /// <returns>A hashcode for this Question</returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode() ^
                   (-1 * Site.GetHashCode());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Clear()
        {
            ReputationGraph.Clear();
            Utils.ICachedImpl(this);
        }
    }

    /// <summary>
    /// Class for managing the internal cache of all
    /// allocated Users.
    /// 
    /// Used so that allocating new Users doesn't result in
    /// duplicate requests to populate it.
    /// </summary>
    internal sealed class UserCache : AllocationCache<User> { }
}
